package top.beiermode.geektime.projects.user.repository;

import top.beiermode.geektime.projects.user.domain.User;
import top.beiermode.geektime.projects.user.sql.DBConnectionManager;
import top.beiermode.geektime.function.ThrowableFunction;
import org.apache.commons.lang.ClassUtils;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 基于数据库的用户仓储
 *
 * @author Chen Song
 * @since 1.0
 */
public class DataBaseUserRepository implements UserRepository {

    private static Logger logger = Logger.getLogger(DataBaseUserRepository.class.getName());

    private static Consumer<Throwable> COMMON_EXCEPTION_HANDLER = e -> logger.log(Level.SEVERE, e.getMessage());

    public static final String INSERT_USER_DML_SQL = "INSERT INTO users(name,password,email,phoneNumber) VALUES (?,?,?,?)";

    public static final String QUERY_ALL_USERS_DML_SQL = "SELECT id,name,password,email,phoneNumber FROM users";

    private final DBConnectionManager dbConnectionManager;

    public DataBaseUserRepository() {
        dbConnectionManager = new DBConnectionManager();
    }

    public DataBaseUserRepository(DBConnectionManager dbConnectionManager) {
        this.dbConnectionManager = dbConnectionManager;
    }

    private Connection getConnection() {
        return dbConnectionManager.getConnection();
    }

    @Override
    public boolean save(User user) {
        return executeUpdate(INSERT_USER_DML_SQL, e -> {
            // TODO
        },user.getName(), user.getPassword(), user.getEmail(), user.getPhoneNumber()) > 0;
    }

    @Override
    public boolean deleteById(Long userId) {
        return false;
    }

    @Override
    public boolean update(User user) {
        return false;
    }

    @Override
    public User getById(Long userId) {
        return null;
    }

    @Override
    public User getByNameAndPassword(String userName, String password) {
        return null;
    }

    @Override
    public Collection<User> getAll() {
        return executeQuery(QUERY_ALL_USERS_DML_SQL, resultSet -> {
            BeanInfo beanInfo = Introspector.getBeanInfo(User.class, Object.class);
            List<User> users = new ArrayList<>();
            while (resultSet.next()) {
                User user = new User();
                for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                    String fieldName = propertyDescriptor.getName();
                    Class<?> fieldType = propertyDescriptor.getPropertyType();
                    String methodName = resultSetMethodMappings.get(fieldType);
                    String columnLabel = mapColumnLabel(fieldName);
                    // 调用 getXXX 方法
                    Method resultSetMethod = ResultSet.class.getMethod(methodName, String.class);
                    Object resultValue = resultSetMethod.invoke(resultSet, columnLabel);
                    // 调用 user setXXX 方法
                    Method setterMethodFromUser = propertyDescriptor.getWriteMethod();
                    setterMethodFromUser.invoke(user, resultValue);
                }
                users.add(user);
            }
            return users;
        }, e -> {
            // TODO 异常处理
        });
    }

    protected int executeUpdate(String sql, Consumer<Throwable> exceptionHandler, Object... args) {
        PreparedStatement preparedStatement = getPreparedStatement(sql, exceptionHandler, args);
        if (preparedStatement == null) {
            throw new RuntimeException("fail to getPreparedStatement.");
        }

        try {
            return preparedStatement.executeUpdate();
        } catch (Throwable e) {
            exceptionHandler.accept(e);
        }
        return 0;
    }

    protected <T> T executeQuery(String sql, ThrowableFunction<ResultSet, T> function,
                                 Consumer<Throwable> exceptionHandler, Object... args) {
        PreparedStatement preparedStatement = getPreparedStatement(sql, exceptionHandler, args);
        if (preparedStatement == null) {
            throw new RuntimeException("fail to getPreparedStatement.");
        }
        try {
            ResultSet resultSet = preparedStatement.executeQuery();
            return function.apply(resultSet);
        } catch (Throwable e) {
            exceptionHandler.accept(e);
        }
        return null;
    }

    private PreparedStatement getPreparedStatement(String sql, Consumer<Throwable> exceptionHandler, Object... args) {
        Connection connection = getConnection();
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            System.out.println(args);
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                Class argType = arg.getClass();

                Class primitiveType = ClassUtils.wrapperToPrimitive(argType);
                if (primitiveType == null) {
                    primitiveType = argType;
                }

                String methodName = preparedStatementMethodMappings.get(argType);
                Method method = PreparedStatement.class.getMethod(methodName, int.class, primitiveType);
                method.invoke(preparedStatement, i + 1, arg);
            }
            return preparedStatement;
        } catch (Throwable e) {
            e.printStackTrace();
            exceptionHandler.accept(e);
        }
        return null;
    }

    private String mapColumnLabel(String fieldName) {
        return fieldName;
    }

    public static Map<Class, String> resultSetMethodMappings = new HashMap<>();

    public static Map<Class, String> preparedStatementMethodMappings = new HashMap<>();

    static {
        resultSetMethodMappings.put(Long.class, "getLong");
        resultSetMethodMappings.put(String.class, "getString");

        preparedStatementMethodMappings.put(Long.class, "setLong"); // long
        preparedStatementMethodMappings.put(String.class, "setString"); // String
    }
}
